Django REST Framework で認証保護
Django で 認証保護を行う方法には主に次のようなものがあります。
Django-RESST-Auth と連携もできる。
今回は Django-REST-Auth と Django-REST-Framework-JWT、django-allauth を組み合わせて認証保護をするようにします。
インストール
Django-REST-Auth と Django-REST-Framework-JWTをインストールしておきましょう。
code: bash
$ pip install django-rest-auth djangorestframework-jwt django-allauth
必要な設定内容の整理
前回のDRF説明は公開されたAPIなのでユーザ情報を保持するデータベースは持っていませんでした。
DRFを使ってJWT認証でWebサービスを保護するためには、次の作業が必要になります。
1. Djangoのベースユーザーモデルから拡張されたカスタムユーザーモデルを作成する
2. カスタムユーザーモデルを管理パネルに登録する
3. 新しいユーザーを登録するためのAPIエンドポイントを作成する
4. ユーザーログイン用のAPIエンドポイントを作成します(ログインにメールを使用)
5. すべてのユーザーをリストするためのAPIエンドポイントを作成する
6. ユーザーを更新するためのAPIエンドポイントを作成する
7. ユーザーを削除するためのAPIエンドポイントを作成する
8. 電子メール/パスワードでログインするAPIエンドポイントを作成します
9. 認証にJWTを使用する
10. カスタムパーミッションを使用してAPIエンドポイントアクセスを制御します
これらを順に勧めていきます。
モデルクラス
django の AbstractUser を継承してカスタムUserモデルを作成します。
code: python
from django.contrib.auth.models import AbstractUser
from django.utils.translation import ugettext_lazy as _
from django.db import models
from django.conf import settings
class Task(models.Model):
id = models.IntegerField(primary_key=True)
title = models.CharField(max_length=32, null=False)
description = models.CharField(max_length=128, null=True)
done = models.BooleanField(default=False)
class Meta:
db_table = 'tasks'
class User(AbstractUser):
username = models.CharField(blank=True, null=True, max_length=32)
email = models.EmailField(_('email address'), unique=True)
USERNAME_FIELD = 'email'
class Meta:
db_table = 'users'
def __str__(self):
return 'f{self.email}'
class UserProfile(models.Model):
user = models.OneToOneField(settings.AUTH_USER_MODEL,
on_delete=models.CASCADE, related_name='profile')
シリアライザを作成
code: python
from rest_framework import serializers
from apiv1.models import Task, User, UserProfile
class TaskSerializer(serializers.ModelSerializer):
class Meta:
model = Task
fields = '__all__'
class UserProfileSerializer(serializers.ModelSerializer):
class Meta:
model = UserProfile
fields = ('user')
class UserSerializer(serializers.HyperlinkedModelSerializer):
profile = UserProfileSerializer(required=True)
class Meta:
model = User
fields = ('url', 'email', 'first_name', 'last_name',
'password', 'profile')
extra_kwargs = {'password': {'write_only': True}}
def create(self, validated_data):
profile_data = validated_data.pop('profile')
password = validated_data.pop('password')
user = User(**validated_data)
user.set_password(password)
user.save()
UserProfile.objects.create(user=user, **profile_data)
return user
def update(self, instance, validated_data):
profile_data = validated_data.pop('profile')
profile = instance.profile
instance.email = validated_data.get('email', instance.email)
instance.save()
profile.username = profile_data.get('username', profile.username)
profile.email = profile_data.get('email', profile.email)
profile.save()
return instance
UserProfileSerializer はfieldsタプルで指定されたフィールドに基づいてUserProfileモデルをシリアル化する単純なModelSerializerです。
UserSerializerは少し複雑で、UserProfileをUserモデルの一部としてシリアル化/逆シリアル化する必要があります。
モデルビューセットを作成
code: python
rom django.shortcuts import render
from django.core import serializers
from django.http import HttpResponse
from rest_framework import viewsets, routers
from apiv1.models import Task, User
from apiv1.serializers import TaskSerializer, UserSerializer
class TaskViewSet(viewsets.ModelViewSet):
queryset = Task.objects.all()
serializer_class = TaskSerializer
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
AUTH_USER_MODEL を登録
todo/settings.py に AUTH_USER_MODEL='アプリケーション名.モデルクラス名' を追加します。
code: python
INSTALLED_APPS = [
# ...
'rest_framework',
'rest_framework.authtoken',
'rest_auth',
'rest_auth.registration',
'apiv1',
]
AUTH_USER_MODEL='apiv1.User'
ユーザモデルを管理画面に登録
code: python
from django.contrib import admin
from django.utils.translation import ugettext_lazy as _
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from .models import User, UserProfile
class UserProfileInline(admin.StackedInline):
model = UserProfile
can_delete = False
@admin.register(User)
class UserAdmin(BaseUserAdmin):
fieldsets = (
(None, {'fields': ('email', 'password')}),
(_('Personal info'), {'fields': ('first_name', 'last_name')}),
(_('Permissions'), {'fields': ('is_active', 'is_staff', 'is_superuser',
'groups', 'user_permissions')}),
(_('Important dates'), {'fields': ('last_login', 'date_joined')}),
)
add_fieldsets = (
(None, {
'classes': ('wide',),
'fields': ('email', 'password1', 'password2'),
}),
)
list_display = ('email', 'first_name', 'last_name', 'is_staff')
search_fields = ('email', 'first_name', 'last_name')
ordering = ('email',)
inlines = (UserProfileInline, )
データベースへ反映
code: bash
$ python manage.py makemigrations
$ python manage.py migrate
$ python manage.py createsuperuser
管理画面からユーザを追加
Django 管理画面に作成したスーパーユーザでログインします。
http://127.0.0.1:8080/admin
https://gyazo.com/b639da19d05c8c6a99eb5317f0c955a0
Users の +Add をクリックして認証に使用するユーザを登録します。
https://gyazo.com/a4e43fbf1f1d16ca0ec79f119755bbfb
code: python
INSTALLED_APPS = [
# ...
'rest_framework',
'rest_framework.authtoken',
'rest_auth',
'rest_auth.registration',
]
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
),
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.BasicAuthentication',
),
}
REST_USE_JWT = True
import datetime
JWT_AUTH = {
'JWT_SECRET_KEY': SECRET_KEY,
'JWT_ALGORITHM': 'HS256',
'JWT_ALLOW_REFRESH': True,
'JWT_EXPIRATION_DELTA': datetime.timedelta(days=7),
'JWT_REFRESH_EXPIRATION_DELTA': datetime.timedelta(days=28),
}
DRF のすべてのグローバル設定は、settings.py の REST_FRAMEWORK という辞書に保持されます。 例えば、アクセス権限を設定するためには次のように行います。
code: python
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly'
]
}
todo/urls.py
code: python
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from rest_framework_jwt.views import (
obtain_jwt_token, verify_jwt_token, refresh_jwt_token
)
urlpatterns = [
path('admin/', admin.site.urls),
path('todo/api/v1.0/', include('todo_apiv1.urls')),
]
apiv1/urls.py
code: python
from django.urls import include, path
from rest_framework import routers
from . import views
router = routers.DefaultRouter()
router.register(r'tasks', views.TaskViewSet)
urlpatterns = [
path('', include(router.urls)),
path('auth/', include('rest_auth.urls')),
]
DRFの説明でタスク一覧を取得したときと同じようにアクセスしてみると、認証エラーとなりアクセスできなくなっています。
http://127.0.0.1:8080/todo/api/v1.0/tasks
https://gyazo.com/c6f86d227197b419b687ca9fb71fceb6
次に、ブラウザで{APIRoot}/auth/login にアクセスしましょう。
http://127.0.0.1:8080/todo/api/v1.0/auth/login
https://gyazo.com/7716f4ad444563586bec0290e6a89923
このページの下部にある入力フォームに Email とPassword に、登録したAPIユーザのものを与えてPOSTをクリックして、今回導入したログイン機能を試してみましょう。
(ユーザー名フィールドは無視できます)
ログインに成功すると、レスポンスにはJWTを含むJSONが含まれています。
https://gyazo.com/c351f1fe457ccb871ffdc769ac0e3521
code: bash
{
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoyLCJ1c2VybmFtZSI6ImZyZWRkaWVAZXhhbXBsZS5jb20iLCJleHAiOjE1OTE3NjI5NDcsImVtYWlsIjoiZnJlZGRpZUBleGFtcGxlLmNvbSIsIm9yaWdfaWF0IjoxNTkxMTU4MTQ3fQ.sFMbB9jzbUNYHwyWqo25js00fezaZ7-Ijh1wVULgETU",
"user": {
"pk": 2,
"username": null,
"email": "freddie@example.com",
"first_name": "",
"last_name": ""
}
}
code: bash
$ curl -s -H "Content-Type: aption/json" -H "Authorization: JWT eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoyLCJ1c2VybmFtZSI6ImZyZWRkaWVAZXhhbXBsZS5jb20iLCJleHAiOjE1OTE3NjI5NDcsImVtYWlsIjoiZnJlZGRpZUBleGFtcGxlLmNvbSIsIm9yaWdfaWF0IjoxNTkxMTU4MTQ3fQ.sFMbB9jzbUNYHwyWqo25js00fezaZ7-Ijh1wVULgETU" -X GET http://127.0.0.1:8080/todo/api/v1.0/tasks/ | python -m json.tool [
{
"id": 1,
"title": "Go Symnastics",
"description": "Go to Anytime Fitness",
"done": false
}
]
JWT認証でタスクを取得できるようになりました。
アクセス権限の設定
タスクについてJWT認証による保護ということについてはほぼ終了していますが、ここまでのコードはまだうまくありません。それは、ログインに成功したすべてのユーザーは、URLにアクセスするだけで他のユーザー情報にアクセスできてしまうことです。
次の許可モデルが必要です。
1. 管理者ユーザーのみがすべてのユーザーのリストを取得できます
2. 管理者のみがユーザーを削除できます
3. 誰でもユーザーを作成できます(登録)
4. ログインしたユーザーは、自分のプロファイルのみを取得/更新できます。
apiv1/permissions.py
code: python
from rest_framework import permissions
class IsLoggedInUserOrAdmin(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
return obj == request.user or request.user.is_staff
class IsAdminUser(permissions.BasePermission):
def has_permission(self, request, view):
return request.user and request.user.is_staff
def has_object_permission(self, request, view, obj):
return request.user and request.user.is_staff
ここで、has_permission() が一般的なレベルのアクセスを許可フラグになり、has_object_permission() が特定のオブジェクトへのアクセスの許可フラグとなります。
code: python
from django.shortcuts import render
from django.core import serializers
from django.http import HttpResponse
from rest_framework import viewsets
from rest_framework.permissions import AllowAny
from apiv1.models import Task, User
from apiv1.serializers import TaskSerializer, UserSerializer
from apiv1.permissions import IsLoggedInUserOrAdmin, IsAdminUser
class TaskViewSet(viewsets.ModelViewSet):
queryset = Task.objects.all()
serializer_class = TaskSerializer
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
def get_permissions(self):
permission_classes = []
if self.action == 'create':
elif (self.action == 'retrieve'
or self.action == 'update'
or self.action == 'partial_update'):
elif self.action == 'list' or self.action == 'destroy':
今回はユーザ情報についてアクセス権限を設定していますが、タスクについても同様になります。